a tool for shared writing and social publishing
at debug/datetime 222 lines 7.8 kB view raw
1"use client"; 2import { publishToPublication } from "actions/publishToPublication"; 3import { DotLoader } from "components/utils/DotLoader"; 4import { useState, useRef } from "react"; 5import { ButtonPrimary } from "components/Buttons"; 6import { Radio } from "components/Checkbox"; 7import { useParams } from "next/navigation"; 8import Link from "next/link"; 9import { AutosizeTextarea } from "components/utils/AutosizeTextarea"; 10import { PubLeafletPublication } from "lexicons/api"; 11import { publishPostToBsky } from "./publishBskyPost"; 12import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 13import { AtUri } from "@atproto/syntax"; 14import { PublishIllustration } from "./PublishIllustration/PublishIllustration"; 15import { useReplicache } from "src/replicache"; 16import { 17 BlueskyPostEditorProsemirror, 18 editorStateToFacetedText, 19} from "./BskyPostEditorProsemirror"; 20import { EditorState } from "prosemirror-state"; 21 22type Props = { 23 title: string; 24 leaflet_id: string; 25 root_entity: string; 26 profile: ProfileViewDetailed; 27 description: string; 28 publication_uri: string; 29 record?: PubLeafletPublication.Record; 30 posts_in_pub?: number; 31}; 32 33export function PublishPost(props: Props) { 34 let [publishState, setPublishState] = useState< 35 { state: "default" } | { state: "success"; post_url: string } 36 >({ state: "default" }); 37 return ( 38 <div className="publishPage w-screen h-full bg-bg-page flex sm:pt-0 pt-4 sm:place-items-center justify-center"> 39 {publishState.state === "default" ? ( 40 <PublishPostForm setPublishState={setPublishState} {...props} /> 41 ) : ( 42 <PublishPostSuccess 43 record={props.record} 44 publication_uri={props.publication_uri} 45 post_url={publishState.post_url} 46 posts_in_pub={(props.posts_in_pub || 0) + 1} 47 /> 48 )} 49 </div> 50 ); 51} 52 53const PublishPostForm = ( 54 props: { 55 setPublishState: (s: { state: "success"; post_url: string }) => void; 56 } & Props, 57) => { 58 let [shareOption, setShareOption] = useState<"bluesky" | "quiet">("bluesky"); 59 let editorStateRef = useRef<EditorState | null>(null); 60 let [isLoading, setIsLoading] = useState(false); 61 let [charCount, setCharCount] = useState(0); 62 let params = useParams(); 63 let { rep } = useReplicache(); 64 65 async function submit() { 66 if (isLoading) return; 67 setIsLoading(true); 68 await rep?.push(); 69 let doc = await publishToPublication({ 70 root_entity: props.root_entity, 71 publication_uri: props.publication_uri, 72 leaflet_id: props.leaflet_id, 73 title: props.title, 74 description: props.description, 75 }); 76 if (!doc) return; 77 78 let post_url = `https://${props.record?.base_path}/${doc.rkey}`; 79 let [text, facets] = editorStateRef.current 80 ? editorStateToFacetedText(editorStateRef.current) 81 : []; 82 if (shareOption === "bluesky") 83 await publishPostToBsky({ 84 facets: facets || [], 85 text: text || "", 86 title: props.title, 87 url: post_url, 88 description: props.description, 89 document_record: doc.record, 90 rkey: doc.rkey, 91 }); 92 setIsLoading(false); 93 props.setPublishState({ state: "success", post_url }); 94 } 95 96 return ( 97 <div className="flex flex-col gap-4 w-[640px] max-w-full sm:px-4 px-3"> 98 <h3>Publish Options</h3> 99 <form 100 onSubmit={(e) => { 101 e.preventDefault(); 102 submit(); 103 }} 104 > 105 <div className="container flex flex-col gap-2 sm:p-3 p-4"> 106 <Radio 107 checked={shareOption === "quiet"} 108 onChange={(e) => { 109 if (e.target === e.currentTarget) { 110 setShareOption("quiet"); 111 } 112 }} 113 name="share-options" 114 id="share-quietly" 115 value="Share Quietly" 116 > 117 <div className="flex flex-col"> 118 <div className="font-bold">Share Quietly</div> 119 <div className="text-sm text-tertiary font-normal"> 120 No one will be notified about this post 121 </div> 122 </div> 123 </Radio> 124 <Radio 125 checked={shareOption === "bluesky"} 126 onChange={(e) => { 127 if (e.target === e.currentTarget) { 128 setShareOption("bluesky"); 129 } 130 }} 131 name="share-options" 132 id="share-bsky" 133 value="Share on Bluesky" 134 > 135 <div className="flex flex-col"> 136 <div className="font-bold">Share on Bluesky</div> 137 <div className="text-sm text-tertiary font-normal"> 138 Pub subscribers will be updated via a custom Bluesky feed 139 </div> 140 </div> 141 </Radio> 142 143 <div 144 className={`w-full pl-5 pb-4 ${shareOption !== "bluesky" ? "opacity-50" : ""}`} 145 > 146 <div className="opaque-container p-3 rounded-lg!"> 147 <div className="flex gap-2"> 148 <img 149 className="rounded-full w-[42px] h-[42px] shrink-0" 150 src={props.profile.avatar} 151 /> 152 <div className="flex flex-col w-full"> 153 <div className="flex gap-2 pb-1"> 154 <p className="font-bold">{props.profile.displayName}</p> 155 <p className="text-tertiary">@{props.profile.handle}</p> 156 </div> 157 <div className="flex flex-col"> 158 <BlueskyPostEditorProsemirror 159 editorStateRef={editorStateRef} 160 onCharCountChange={setCharCount} 161 /> 162 </div> 163 <div className="opaque-container overflow-hidden flex flex-col mt-4 w-full"> 164 {/* <div className="h-[260px] w-full bg-test" /> */} 165 <div className="flex flex-col p-2"> 166 <div className="font-bold">{props.title}</div> 167 <div className="text-tertiary">{props.description}</div> 168 <hr className="border-border-light mt-2 mb-1" /> 169 <p className="text-xs text-tertiary"> 170 {props.record?.base_path} 171 </p> 172 </div> 173 </div> 174 <div className="text-xs text-secondary italic place-self-end pt-2"> 175 {charCount}/300 176 </div> 177 </div> 178 </div> 179 </div> 180 </div> 181 <div className="flex justify-between"> 182 <Link 183 className="hover:no-underline! font-bold" 184 href={`/${params.leaflet_id}`} 185 > 186 Back 187 </Link> 188 <ButtonPrimary 189 type="submit" 190 className="place-self-end h-[30px]" 191 disabled={charCount > 300} 192 > 193 {isLoading ? <DotLoader /> : "Publish this Post!"} 194 </ButtonPrimary> 195 </div> 196 </div> 197 </form> 198 </div> 199 ); 200}; 201 202const PublishPostSuccess = (props: { 203 post_url: string; 204 publication_uri: string; 205 record: Props["record"]; 206 posts_in_pub: number; 207}) => { 208 let uri = new AtUri(props.publication_uri); 209 return ( 210 <div className="container p-4 m-3 sm:m-4 flex flex-col gap-1 justify-center text-center w-fit h-fit mx-auto"> 211 <PublishIllustration posts_in_pub={props.posts_in_pub} /> 212 <h2 className="pt-2">Published!</h2> 213 <Link 214 className="hover:no-underline! font-bold place-self-center pt-2" 215 href={`/lish/${uri.host}/${encodeURIComponent(props.record?.name || "")}/dashboard`} 216 > 217 <ButtonPrimary>Back to Dashboard</ButtonPrimary> 218 </Link> 219 <a href={props.post_url}>See published post</a> 220 </div> 221 ); 222};